(function main() {
    const ground = document.getElementById('ground'),
        control = document.getElementById('control'),
        [header, score, start, rank] = control.children,
        dialog = document.getElementById('dial'),
        inputName = document.getElementById('uname'),
        submitBtn = document.getElementById('dial-btn-group').children[0],
        rankings = document.getElementById('rankings'),
        recordList = document.getElementById('records'),
        contentBox = document.getElementById('content');

    const ctx = ground.getContext('2d'),
        len = 600,
        divides = 20,
        step = len / divides,
        radius = step / 3,
        fat = radius / 2,
        initSize = 5,
        initSpeed = 400,
        speedUpRate = 3 / 4,
        food = [0, 0],
        lightning = [0, 0],
        poison = [0, 0],
        maxCountdowns = [30, 20, 10],
        countdowns = maxCountdowns.slice(),
        PI = Math.PI,
        maxRecordNum = 3,
        maxNameByteLen = 12,
        rescale = Math.min(document.defaultView.innerWidth, document.defaultView.innerHeight) * 0.9 / len;

    const records = JSON.parse(localStorage.getItem('records')) || [];

    ground.setAttribute('width', len.toString());
    ground.setAttribute('height', len.toString());

    contentBox.style.transform = `scale(${rescale})`;
    dialog.style.transform = `translate(-50%, -50%) scale(${rescale})`;
    rankings.style.transform = `translate(-50%, -50%) scale(${rescale})`;

    class Utils {
        encode(pos) {
            return pos[0] + '&' + pos[1];
        }

        randomPos() {
            return [Math.floor(Math.random() * (divides-1)) + 1,
                Math.floor(Math.random() * (divides-1)) + 1]
        }

        randomFood() {
            countdowns[0] = maxCountdowns[0];
            let [x, y] = this.randomPos();
            while(snake.querySet.has(this.encode([x, y]))
            || (x === poison[0] && y === poison[1]) || (x === lightning[0] && y === lightning[1])) {
                [x, y] = this.randomPos();
            }
            food[0] = x;
            food[1] = y;
            draw.drawFood();
        }

        randomLightning() {
            countdowns[1] = maxCountdowns[1];
            let [x, y] = this.randomPos();
            while(snake.querySet.has(this.encode([x, y]))
            || (x === food[0] && y === food[1]) || (x === poison[0] && y === poison[1])) {
                [x, y] = this.randomPos();
            }
            lightning[0] = x;
            lightning[1] = y;
            draw.drawLightning();
        }

        randomPoison() {
            countdowns[2] = maxCountdowns[2];
            let [x, y] = this.randomPos();
            while(snake.querySet.has(this.encode([x, y]))
            || (x === food[0] && y === food[1]) || (x === lightning[0] && y === lightning[1])) {
                [x, y] = this.randomPos();
            }
            poison[0] = x;
            poison[1] = y;
            draw.drawPoison();
        }

        getRealPos(...args) {
            return args.map(v => v * step)
        }

        debounce(cb, delay) {
            let timer = null;
            return function (...args) {
                if (timer) clearTimeout(timer);
                timer = setTimeout(cb.bind(this, ...args), delay);
            }
        }

        gameStart() {
            snake = new Snake();
            utils.randomFood();
            utils.randomLightning();
            utils.randomPoison();
            snake.isMoving = true;
            control.style.display = 'none';
            document.body.onkeydown = snake.turn.bind(snake);
            document.body.ontouchstart = snake.startTouch.bind(snake);
            document.body.ontouchend = snake.endTouch.bind(snake);
            document.body.addEventListener('touchmove', (e) => {
                if (snake.isMoving) e.preventDefault();
            }, {passive: false});
            maxCountdowns[0] = 30; maxCountdowns[1] = 20; maxCountdowns[2] = 10;
            countdowns.forEach((v, i) => countdowns[i] = maxCountdowns[i]);
            snake.move();
        }

        countScore() {
            return (snake.points.length - initSize)
                << Math.round(Math.log2(snake.speed / initSpeed) / Math.log2(speedUpRate))
        }

        fail() {
            const res = this.countScore();
            header.innerText = '游戏结束';
            score.innerText = '分数：' + res;
            control.style.display = 'flex';
            const rl = records.length;
            if (rl < maxRecordNum || res > records[rl-1]['score']) {
                dialog.showModal();
                if (rl === maxRecordNum) records.pop();
            }
            document.body.onkeydown = null;
        }

        createRankings() {
            const frag = new DocumentFragment();
            for (let i=0; i<maxRecordNum; i++) {
                const record = document.createElement('div');
                record.classList.add('record-item');
                for (let j=0; j<4; j++) {
                    record.append(document.createElement('div'));
                }
                frag.append(record);
            }
            recordList.append(frag);
            this.updateRankings();
        }

        limitByteLength(str, maxLen) {
            let count = 0;
            for (let i=0; i<str.length; i++) {
                count += str.charCodeAt(i) > 255 ? 2 : 1;
                if (count > maxLen) return str.slice(0, i);
            }
            return str
        }

        updateRecords(res, name) {
            if (name === '') return
            records.push({
                name,
                score: res,
                time: Date.now()
            })
            records.sort((a, b) =>  b['score'] - a['score'] || a['time'] - b['time']);
            localStorage.setItem('records', JSON.stringify(records));
            this.updateRankings();
        }

        updateRankings() {
            records.forEach((record, i) => {
                const item = recordList.children[++i];
                item.children[0].innerText = (i).toString();
                item.children[1].innerText = record['name'];
                item.children[2].innerText = record['score'];
                const T = new Date(record['time']);
                item.children[3].innerText = `${T.getFullYear()}/${T.getMonth()+1}/${T.getDate()}`
            })
        }
    }

    class Draw {
        drawFood() {
            const [x, y] = utils.getRealPos(...food);
            ctx.beginPath();
            ctx.fillStyle = 'rgba(224, 64, 39, 1)';
            ctx.arc(x - radius / 3, y, radius, 0, PI * 2);
            ctx.arc(x + radius / 3, y, radius, 0, PI * 2);
            ctx.fill();
        }

        drawLightning() {
            const [x, y] = utils.getRealPos(...lightning);
            ctx.fillStyle = 'yellow';
            ctx.beginPath();
            const m = radius * 1.5,
                n = m / 3;
            ctx.moveTo(x, y - m);
            ctx.lineTo(x, y - n);
            ctx.lineTo(x + m, y - n);
            ctx.lineTo(x, y + m);
            ctx.lineTo(x, y + n);
            ctx.lineTo(x - m, y + n);
            ctx.fill();
        }

        drawPoison() {
            const [x, y] = utils.getRealPos(...poison),
                r = radius * 1.5;
            ctx.fillStyle = 'purple';
            ctx.beginPath();
            ctx.arc(x, y, r, PI, PI * 2);
            ctx.fill();
            ctx.fillStyle = 'white';
            ctx.beginPath();
            ctx.fillRect(x - r / 2, y, r, r);
        }

        drawHead(x, y, direction) {
            [x, y] = utils.getRealPos(x, y);
            ctx.fillStyle = 'rgba(16, 113, 10, 1)';
            ctx.beginPath();
            ctx.arc(x, y, radius, PI/4 + PI/2*direction, -PI/4 + PI/2*direction);
            ctx.lineTo(x, y);
            ctx.fill();
        }

        drawTail(preX, preY, x, y) {
            [preX, preY, x, y] = utils.getRealPos(preX, preY, x, y);
            let pre1, pre2;
            if (preX !== x) {
                pre1 = [preX, y - fat];
                pre2 = [preX, y + fat];
            } else {
                pre1 = [x - fat, preY];
                pre2 = [x + fat, preY];
            }

            ctx.fillStyle = 'rgba(118, 255, 209, 0.8)';
            ctx.beginPath();
            ctx.moveTo(x, y);
            ctx.lineTo(pre1[0], pre1[1]);
            ctx.lineTo(pre2[0], pre2[1]);
            ctx.fill();
        }

        // 根据当前节点与前一节点的相对位置，确定构建矩形的输入参数
        getRectParams(preX, preY, x, y) {
            let sx, sy, sw, sh;
            if (preX > x) {
                sx = x; sy = y - fat;
                sw = step; sh = fat << 1;
            } else if (preY > y) {
                sx = x - fat; sy = y;
                sw = fat << 1; sh = step;
            } else if (preX < x) {
                sx = preX; sy = preY - fat;
                sw = step; sh = fat << 1;
            } else {
                sx = preX - fat; sy = preY;
                sw = fat << 1; sh = step;
            }
            return [sx, sy, sw, sh]
        }

        drawBody(preX, preY, x, y) {
            [preX, preY, x, y] = utils.getRealPos(preX, preY, x, y);

            const [sx, sy, sw, sh] = this.getRectParams(preX, preY, x, y);
            let d = 4;

            ctx.beginPath();
            // 蛇的中段
            ctx.fillStyle = 'rgba(16, 113, 10, 1)';
            ctx.fillRect(sx, sy, sw, sh);

            // 蛇的环节
            ctx.fillStyle = 'rgba(118, 255, 209, 0.8)';
            if (preX !== x) {
                ctx.fillRect(sx, sy, sw / d, sh);
                ctx.fillRect(sx + sw * (d - 1) / d, sy, sw / d, sh);
            } else {
                ctx.fillRect(sx, sy, sw, sh / d);
                ctx.fillRect(sx, sy + sh * (d - 1) / d, sw, sh / d);
            }
        }

        drawSnake(points, direction) {
            ctx.clearRect(0, 0, len, len);
            const size = points.length;
            for (let i=1; i<size-1; i++) {
                this.drawBody(...points[i-1], ...points[i]);
            }

            this.drawHead(...points[0], direction);
            this.drawTail(...points[size-2], ...points[size-1]);
        }

        clearUnit(cx, cy) {
            [cx, cy] = utils.getRealPos(cx, cy);
            ctx.clearRect(cx - step / 2, cy - step / 2, step, step)
        }

        clearSegment(preX, preY, x, y) {
            ctx.clearRect(...this.getRectParams(...utils.getRealPos(preX, preY, x, y)));
        }

        updateSnake(newHead, oldHead, preOldHead, direction, preNewTail, newTail, oldTail) {
            this.clearUnit(...newHead);
            this.clearUnit(...oldHead);

            if (oldTail) {
                this.clearSegment(...newTail, ...oldTail);
                this.clearSegment(...preNewTail, ...newTail);
                this.drawTail(...preNewTail, ...newTail);
            }

            this.drawBody(...newHead, ...oldHead);
            this.drawBody(...oldHead, ...preOldHead);
            this.drawHead(...newHead, direction);
        }
    }

    class Snake {
        constructor() {
            this.direction = 0;
            this.nextDirection = 0;
            this.speed = initSpeed;
            this.isMoving = false;
            this.touchX = 0;
            this.touchY = 0;

            const middle = divides >> 1;
            this.points = [];
            for (let i=0; i<initSize; i++) {
                this.points.push([middle + (initSize >> 1) - i, middle])
            }
            this.querySet = new Set(this.points.map(pos => utils.encode(pos)));
            draw.drawSnake(this.points, this.direction);
        }

        move() {
            const [preHeadX, preHeadY] = this.points[0];
            let nxt;

            this.direction = this.nextDirection;
            switch (this.direction) {
                case 0: nxt = [preHeadX + 1, preHeadY]; break;
                case 1: nxt = [preHeadX, preHeadY + 1]; break;
                case 2: nxt = [preHeadX - 1, preHeadY]; break;
                case 3: nxt = [preHeadX, preHeadY - 1]; break;
                default: throw new Error('Unexpected Direction');
            }

            let oldTail = null;
            if (nxt[0] !== food[0] || nxt[1] !== food[1]) {
                oldTail = this.points.pop();
                this.querySet.delete(utils.encode(oldTail));
            }

            // 检测是否撞到墙边或者自身，或吃到毒蘑菇
            if (nxt[0] === 0 || nxt[1] === 0 || nxt[0] === divides || nxt[1] === divides
                || this.querySet.has(utils.encode(nxt))
                || (nxt[0] === poison[0] && nxt[1] === poison[1])) {
                this.isMoving = false;
                if (oldTail) this.points.push([-1,-1]); // 用来加上被提前去除的尾部
                utils.fail();
                return
            }

            this.points.unshift(nxt);
            this.querySet.add(utils.encode(nxt));
            // draw.drawSnake(this.points, this.direction);
            draw.updateSnake(...this.points.slice(0,3), this.direction, ...this.points.slice(-2), oldTail);

            if (nxt[0] === food[0] && nxt[1] === food[1]) {
                utils.randomFood();
            } else if (nxt[0] === lightning[0] && nxt[1] === lightning[1]) {
                this.speed *= speedUpRate;
                maxCountdowns.forEach((v, i) => maxCountdowns[i] = Math.round(v / speedUpRate));
                utils.randomLightning();
            }

            countdowns.forEach((countdown, i) => {
                if (--countdowns[i] === 0) {
                    switch (i) {
                        case 0: {
                            draw.clearUnit(...food);
                            utils.randomFood();
                            break;
                        }
                        case 1: {
                            draw.clearUnit(...lightning);
                            utils.randomLightning();
                            break;
                        }
                        case 2: {
                            draw.clearUnit(...poison);
                            utils.randomPoison();
                            break;
                        }
                    }
                }
            })

            setTimeout(() => this.move(), this.speed);
        }

        turn(e) {
            if (this.isMoving) {
                switch (e.key) {
                    case 'd': case 'ArrowRight': if (this.direction !== 2) this.nextDirection = 0; break;
                    case 's': case 'ArrowDown': if (this.direction !== 3) this.nextDirection = 1; break;
                    case 'a': case 'ArrowLeft': if (this.direction !== 0) this.nextDirection = 2; break;
                    case 'w': case 'ArrowUp': if (this.direction !== 1) this.nextDirection = 3; break;
                }
            }
        }

        startTouch(e) {
            if (this.isMoving) {
                e.preventDefault();
                this.touchX = e.targetTouches[0].screenX;
                this.touchY = e.targetTouches[0].screenY;
            }
        }

        endTouch(e) {
            if (this.isMoving) {
                if (this.direction === 0 || this.direction === 2) {
                    if (e.changedTouches[0].screenY > this.touchY) this.nextDirection = 1;
                    else if (e.changedTouches[0].screenY < this.touchY) this.nextDirection = 3;
                } else {
                    if (e.changedTouches[0].screenX > this.touchX) this.nextDirection = 0;
                    else if (e.changedTouches[0].screenX < this.touchX) this.nextDirection = 2;
                }
            }
        }
    }

    const utils = new Utils(),
        draw = new Draw();
    let snake;

    start.onclick = utils.debounce(utils.gameStart, 200);

    inputName.oninput = () => inputName.value = utils.limitByteLength(inputName.value, maxNameByteLen);
    inputName.onchange = () => submitBtn.value = inputName.value;
    dialog.onclose = () => utils.updateRecords(utils.countScore(), dialog.returnValue);

    rank.onclick = utils.debounce(() => rankings.showModal(), 200);
    utils.createRankings();
})()


